我們今天來創建存儲使用者在本地。
創建user service,並且登入部份我們先使用mock方法來模擬
// src\services\user.service.ts
import { MMKV } from 'react-native-mmkv';
import { User } from '../types';
const secureStorage = new MMKV({
id: 'secure-user-storage',
encryptionKey: 'secret-key',
});
const USER_KEY = 'user';
const IS_LOGGED_IN_KEY = 'isLoggedIn';
const TOKEN_KEY = 'token';
const mockApiCall = <T>(data: T): Promise<T> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(data);
}, 1000);
});
};
export const UserService = {
saveUser: (user: User) => {
secureStorage.set(USER_KEY, JSON.stringify(user));
},
getUser: (): User | null => {
const userJson = secureStorage.getString(USER_KEY);
return userJson ? JSON.parse(userJson) : null;
},
setLoggedIn: (isLoggedIn: boolean) => {
secureStorage.set(IS_LOGGED_IN_KEY, isLoggedIn);
},
isLoggedIn: (): boolean => {
return secureStorage.getBoolean(IS_LOGGED_IN_KEY) || false;
},
saveToken: (token: string) => {
secureStorage.set(TOKEN_KEY, token);
},
getToken: (): string | null => {
return secureStorage.getString(TOKEN_KEY) || null;
},
clearUserData: () => {
secureStorage.delete(USER_KEY);
secureStorage.delete(IS_LOGGED_IN_KEY);
secureStorage.delete(TOKEN_KEY);
},
login: async (email: string, password: string): Promise<User> => {
const user: User = await mockApiCall({
id: '1',
username: email.split('@')[0],
email: email,
createdAt: new Date().toISOString(),
});
const token = 'mock-jwt-token';
UserService.saveUser(user);
UserService.setLoggedIn(true);
UserService.saveToken(token);
return user;
},
logout: () => {
UserService.clearUserData();
},
};
修改並添加user到我們的atom
// src\stores\atoms.ts
import { atom } from 'jotai';
import { Task, User } from '../types';
import { TaskService } from '../services/task.service';
import { UserService } from '../services/user.service';
export const userAtom = atom<User | null>(UserService.getUser());
export const isLoggedInAtom = atom<boolean>(UserService.isLoggedIn());
export const tokenAtom = atom<string | null>(UserService.getToken());
export const tasksAtom = atom<Task[]>(TaskService.getTasks());
接下來我們創建Login Page
// src\pages\login.page.tsx
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native';
import { useSetAtom } from 'jotai';
import { userAtom, isLoggedInAtom } from '../stores/atoms';
import { UserService } from '../services/user.service';
type LoginPageProps = {
navigation: any;
};
const LoginPage: React.FC<LoginPageProps> = ({ navigation }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const setUser = useSetAtom(userAtom);
const setIsLoggedIn = useSetAtom(isLoggedInAtom);
const handleLogin = async () => {
try {
const user = await UserService.login(email, password);
setUser(user);
setIsLoggedIn(true);
navigation.navigate('user');
} catch (error) {
Alert.alert('登錄失敗', '請檢查您的郵箱和密碼');
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>登錄</Text>
<TextInput
style={styles.input}
placeholder="郵箱"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
<TextInput
style={styles.input}
placeholder="密碼"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<TouchableOpacity style={styles.loginButton} onPress={handleLogin}>
<Text style={styles.loginButtonText}>登錄</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
},
input: {
width: '100%',
height: 40,
borderColor: 'gray',
borderWidth: 1,
borderRadius: 5,
marginBottom: 10,
paddingHorizontal: 10,
},
loginButton: {
backgroundColor: '#007AFF',
padding: 10,
borderRadius: 5,
width: '100%',
alignItems: 'center',
},
loginButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
export default LoginPage;
並且在App上添加該page到nav,並稍微修正一下render page的方法。
// App.tsx
// ...
import UserPage from './src/pages/user.page';
import LoginPage from './src/pages/login.page';
function MainContent() {
const [isDarkMode, setIsDarkMode] = useState(useColorScheme() === 'dark');
const [currentPage, setCurrentPage] = useState('home');
const [tasks, setTasks] = useState<Task[]>([]);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const [user, setUser] = useAtom(userAtom);
useEffect(() => {
// 生成隨機任務當應用啟動時
setTasks(generateRandomTasks(10));
// 模擬用戶登入
setUser({
id: '1',
username: 'JohnDoe',
email: 'johndoe@example.com',
createdAt: new Date().toISOString(),
});
}, []);
const toggleDarkMode = () => {
setIsDarkMode(!isDarkMode);
};
const generateRandomTasks = (count: number): Task[] => {
const generateRandomDate = (start: Date, end: Date) => {
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toISOString().split('T')[0];
};
const possibleTags: Tags[] = [
{ title: '工作' },
{ title: '個人' },
{ title: '學習' },
{ title: '娛樂' },
];
for (let i = 0; i < count; i++) {
const startDate = generateRandomDate(new Date(), new Date(Date.now() + 30 * 24 * 60 * 60 * 1000));
const endDate = generateRandomDate(new Date(startDate), new Date(Date.now() + 60 * 24 * 60 * 60 * 1000));
tasks.push({
id: `task-${i + 1}`,
title: `任務 ${i + 1}`,
startDate,
endDate,
description: `隨機生成的第 ${i + 1} 筆內容。`,
isDone: Math.random() < 0.3,
tags: possibleTags.filter(() => Math.random() < 0.3),
subTasks: [],
});
}
return tasks;
};
const renderPage = () => {
const navigation = {
navigate: (pageName: string) => {
setCurrentPage(pageName);
}
};
switch (currentPage) {
case 'home':
return (
<HomePage
isDarkMode={isDarkMode}
tasks={tasks}
setTasks={setTasks}
onTaskSelect={(task) => {
setSelectedTask(task);
setCurrentPage('taskDetails');
}}
/>
);
case 'add':
return <AddTaskPage isDarkMode={isDarkMode} />;
case 'stats':
return <StatisticsPage isDarkMode={isDarkMode} tasks={tasks} />;
case 'user':
return <UserPage navigation={navigation} />;
case 'login':
return <LoginPage navigation={navigation} />;
case 'settings':
return <SettingsPage isDarkMode={isDarkMode} toggleDarkMode={toggleDarkMode} navigation={navigation} />;
case 'taskDetails':
return selectedTask ? (
<TaskDetailsPage
isDarkMode={isDarkMode}
task={selectedTask}
onBack={() => setCurrentPage('home')}
/>
) : null;
default:
return <HomePage isDarkMode={isDarkMode} tasks={tasks} setTasks={setTasks} />;
}
};
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
flex: 1,
};
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaView style={backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
<View style={styles.topArea}>
<Text style={styles.topText}>我的應用</Text>
</View>
<View style={styles.container}>
{renderPage()}
</View>
<View style={styles.bottomNav}>
<TouchableOpacity style={styles.navItem} onPress={() => setCurrentPage('home')}>
<Text style={styles.navText}>主頁</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.navItem} onPress={() => setCurrentPage('add')}>
<Text style={styles.navText}>新增</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.navItem} onPress={() => setCurrentPage('stats')}>
<Text style={styles.navText}>統計</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.navItem} onPress={() => setCurrentPage('user')}>
<Text style={styles.navText}>用戶</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
</GestureHandlerRootView>
);
}
function App(): React.JSX.Element {
return (
<SafeAreaProvider>
<MainContent />
</SafeAreaProvider>
);
}
// ...
接著調整User page,並且在未登入時可以點擊進行登入。
// src\pages\user.page.tsx
import React from 'react';
import { View, Text, StyleSheet, Image, TouchableOpacity, ScrollView } from 'react-native';
import { useAtom } from 'jotai';
import { userAtom, isLoggedInAtom } from '../stores/atoms';
import { UserService } from '../services/user.service';
type UserPageProps = {
navigation: any;
};
const UserPage: React.FC<UserPageProps> = ({ navigation }) => {
const [user, setUser] = useAtom(userAtom);
const [isLoggedIn, setIsLoggedIn] = useAtom(isLoggedInAtom);
const handleLogout = () => {
UserService.logout();
setUser(null);
setIsLoggedIn(false);
};
if (!isLoggedIn) {
return (
<View style={styles.container}>
<Text style={styles.title}>請先登入</Text>
<TouchableOpacity
style={styles.loginButton}
onPress={() => navigation.navigate('login')}
>
<Text style={styles.loginButtonText}>登入</Text>
</TouchableOpacity>
</View>
);
}
return (
<ScrollView style={styles.container}>
<View style={styles.profileHeader}>
<Image
source={{ uri: user?.avatar || 'https://via.placeholder.com/150' }}
style={styles.avatar}
/>
<Text style={styles.username}>{user?.username}</Text>
<Text style={styles.email}>{user?.email}</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>用戶信息</Text>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>ID:</Text>
<Text style={styles.infoValue}>{user?.id}</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>創建日期:</Text>
<Text style={styles.infoValue}>{user?.createdAt ? new Date(user.createdAt).toLocaleDateString() : 'N/A'}</Text>
</View>
</View>
<TouchableOpacity
style={styles.settingsButton}
onPress={() => navigation.navigate('settings')}
>
<Text style={styles.settingsButtonText}>設置</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutButtonText}>登出</Text>
</TouchableOpacity>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
textAlign: 'center',
},
profileHeader: {
alignItems: 'center',
marginBottom: 20,
},
avatar: {
width: 100,
height: 100,
borderRadius: 50,
marginBottom: 10,
},
username: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 5,
},
email: {
fontSize: 16,
color: '#666',
},
section: {
marginBottom: 20,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
},
infoItem: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 5,
},
infoLabel: {
fontWeight: 'bold',
},
infoValue: {
color: '#666',
},
settingsButton: {
backgroundColor: '#007AFF',
padding: 10,
borderRadius: 5,
alignItems: 'center',
marginBottom: 10,
},
settingsButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
loginButton: {
backgroundColor: '#007AFF',
padding: 10,
borderRadius: 5,
alignItems: 'center',
},
loginButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
logoutButton: {
backgroundColor: '#FF3B30',
padding: 10,
borderRadius: 5,
alignItems: 'center',
},
logoutButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
export default UserPage;
今天我們實現了簡易的使用者頁面和登入頁面,明天會先對所有的部分進行調整,並添加路由系統。